// Basic Javascript Elliptic Curve implementation
// Ported loosely from BouncyCastle's Java EC code
// Only Fp curves implemented for now

// Requires jsbn.js and jsbn2.js
import { BigInteger } from 'jsbn'
const { Barrett } = BigInteger.prototype

// Basic Javascript Elliptic Curve implementation
// Ported loosely from BouncyCastle's Java EC code
// Only Fp curves implemented for now

// Requires jsbn.js and jsbn2.js

// ----------------
// ECFieldElementFp

// constructor
function ECFieldElementFp(q, x) {
  this.x = x
  // TODO if(x.compareTo(q) >= 0) error
  this.q = q
}

function feFpEquals(other) {
  if (other == this) return true
  return this.q.equals(other.q) && this.x.equals(other.x)
}

function feFpToBigInteger() {
  return this.x
}

function feFpNegate() {
  return new ECFieldElementFp(this.q, this.x.negate().mod(this.q))
}

function feFpAdd(b) {
  return new ECFieldElementFp(this.q, this.x.add(b.toBigInteger()).mod(this.q))
}

function feFpSubtract(b) {
  return new ECFieldElementFp(
    this.q,
    this.x.subtract(b.toBigInteger()).mod(this.q)
  )
}

function feFpMultiply(b) {
  return new ECFieldElementFp(
    this.q,
    this.x.multiply(b.toBigInteger()).mod(this.q)
  )
}

function feFpSquare() {
  return new ECFieldElementFp(this.q, this.x.square().mod(this.q))
}

function feFpDivide(b) {
  return new ECFieldElementFp(
    this.q,
    this.x.multiply(b.toBigInteger().modInverse(this.q)).mod(this.q)
  )
}

ECFieldElementFp.prototype.equals = feFpEquals
ECFieldElementFp.prototype.toBigInteger = feFpToBigInteger
ECFieldElementFp.prototype.negate = feFpNegate
ECFieldElementFp.prototype.add = feFpAdd
ECFieldElementFp.prototype.subtract = feFpSubtract
ECFieldElementFp.prototype.multiply = feFpMultiply
ECFieldElementFp.prototype.square = feFpSquare
ECFieldElementFp.prototype.divide = feFpDivide

// ----------------
// ECPointFp

// constructor
export function ECPointFp(curve, x, y, z) {
  this.curve = curve
  this.x = x
  this.y = y
  // Projective coordinates: either zinv == null or z * zinv == 1
  // z and zinv are just BigIntegers, not fieldElements
  if (z == null) {
    this.z = BigInteger.ONE
  } else {
    this.z = z
  }
  this.zinv = null
  //TODO: compression flag
}

function pointFpGetX() {
  if (this.zinv == null) {
    this.zinv = this.z.modInverse(this.curve.q)
  }
  var r = this.x.toBigInteger().multiply(this.zinv)
  this.curve.reduce(r)
  return this.curve.fromBigInteger(r)
}

function pointFpGetY() {
  if (this.zinv == null) {
    this.zinv = this.z.modInverse(this.curve.q)
  }
  var r = this.y.toBigInteger().multiply(this.zinv)
  this.curve.reduce(r)
  return this.curve.fromBigInteger(r)
}

function pointFpEquals(other) {
  if (other == this) return true
  if (this.isInfinity()) return other.isInfinity()
  if (other.isInfinity()) return this.isInfinity()
  var u, v
  // u = Y2 * Z1 - Y1 * Z2
  u = other.y
    .toBigInteger()
    .multiply(this.z)
    .subtract(this.y.toBigInteger().multiply(other.z))
    .mod(this.curve.q)
  if (!u.equals(BigInteger.ZERO)) return false
  // v = X2 * Z1 - X1 * Z2
  v = other.x
    .toBigInteger()
    .multiply(this.z)
    .subtract(this.x.toBigInteger().multiply(other.z))
    .mod(this.curve.q)
  return v.equals(BigInteger.ZERO)
}

function pointFpIsInfinity() {
  if (this.x == null && this.y == null) return true
  return (
    this.z.equals(BigInteger.ZERO) &&
    !this.y.toBigInteger().equals(BigInteger.ZERO)
  )
}

function pointFpNegate() {
  return new ECPointFp(this.curve, this.x, this.y.negate(), this.z)
}

function pointFpAdd(b) {
  if (this.isInfinity()) return b
  if (b.isInfinity()) return this

  // u = Y2 * Z1 - Y1 * Z2
  var u = b.y
    .toBigInteger()
    .multiply(this.z)
    .subtract(this.y.toBigInteger().multiply(b.z))
    .mod(this.curve.q)
  // v = X2 * Z1 - X1 * Z2
  var v = b.x
    .toBigInteger()
    .multiply(this.z)
    .subtract(this.x.toBigInteger().multiply(b.z))
    .mod(this.curve.q)

  if (BigInteger.ZERO.equals(v)) {
    if (BigInteger.ZERO.equals(u)) {
      return this.twice() // this == b, so double
    }
    return this.curve.getInfinity() // this = -b, so infinity
  }

  var THREE = new BigInteger('3')
  var x1 = this.x.toBigInteger()
  var y1 = this.y.toBigInteger()
  var x2 = b.x.toBigInteger()
  var y2 = b.y.toBigInteger()

  var v2 = v.square()
  var v3 = v2.multiply(v)
  var x1v2 = x1.multiply(v2)
  var zu2 = u.square().multiply(this.z)

  // x3 = v * (z2 * (z1 * u^2 - 2 * x1 * v^2) - v^3)
  var x3 = zu2
    .subtract(x1v2.shiftLeft(1))
    .multiply(b.z)
    .subtract(v3)
    .multiply(v)
    .mod(this.curve.q)
  // y3 = z2 * (3 * x1 * u * v^2 - y1 * v^3 - z1 * u^3) + u * v^3
  var y3 = x1v2
    .multiply(THREE)
    .multiply(u)
    .subtract(y1.multiply(v3))
    .subtract(zu2.multiply(u))
    .multiply(b.z)
    .add(u.multiply(v3))
    .mod(this.curve.q)
  // z3 = v^3 * z1 * z2
  var z3 = v3.multiply(this.z).multiply(b.z).mod(this.curve.q)

  return new ECPointFp(
    this.curve,
    this.curve.fromBigInteger(x3),
    this.curve.fromBigInteger(y3),
    z3
  )
}

function pointFpTwice() {
  if (this.isInfinity()) return this
  if (this.y.toBigInteger().signum() == 0) return this.curve.getInfinity()

  // TODO: optimized handling of constants
  var THREE = new BigInteger('3')
  var x1 = this.x.toBigInteger()
  var y1 = this.y.toBigInteger()

  var y1z1 = y1.multiply(this.z)
  var y1sqz1 = y1z1.multiply(y1).mod(this.curve.q)
  var a = this.curve.a.toBigInteger()

  // w = 3 * x1^2 + a * z1^2
  var w = x1.square().multiply(THREE)
  if (!BigInteger.ZERO.equals(a)) {
    w = w.add(this.z.square().multiply(a))
  }
  w = w.mod(this.curve.q)
  //this.curve.reduce(w);
  // x3 = 2 * y1 * z1 * (w^2 - 8 * x1 * y1^2 * z1)
  var x3 = w
    .square()
    .subtract(x1.shiftLeft(3).multiply(y1sqz1))
    .shiftLeft(1)
    .multiply(y1z1)
    .mod(this.curve.q)
  // y3 = 4 * y1^2 * z1 * (3 * w * x1 - 2 * y1^2 * z1) - w^3
  var y3 = w
    .multiply(THREE)
    .multiply(x1)
    .subtract(y1sqz1.shiftLeft(1))
    .shiftLeft(2)
    .multiply(y1sqz1)
    .subtract(w.square().multiply(w))
    .mod(this.curve.q)
  // z3 = 8 * (y1 * z1)^3
  var z3 = y1z1.square().multiply(y1z1).shiftLeft(3).mod(this.curve.q)

  return new ECPointFp(
    this.curve,
    this.curve.fromBigInteger(x3),
    this.curve.fromBigInteger(y3),
    z3
  )
}

// Simple NAF (Non-Adjacent Form) multiplication algorithm
// TODO: modularize the multiplication algorithm
function pointFpMultiply(k) {
  if (this.isInfinity()) return this
  if (k.signum() == 0) return this.curve.getInfinity()

  var e = k
  var h = e.multiply(new BigInteger('3'))

  var neg = this.negate()
  var R = this

  var i
  for (i = h.bitLength() - 2; i > 0; --i) {
    R = R.twice()

    var hBit = h.testBit(i)
    var eBit = e.testBit(i)

    if (hBit != eBit) {
      R = R.add(hBit ? this : neg)
    }
  }

  return R
}

// Compute this*j + x*k (simultaneous multiplication)
function pointFpMultiplyTwo(j, x, k) {
  var i
  if (j.bitLength() > k.bitLength()) i = j.bitLength() - 1
  else i = k.bitLength() - 1

  var R = this.curve.getInfinity()
  var both = this.add(x)
  while (i >= 0) {
    R = R.twice()
    if (j.testBit(i)) {
      if (k.testBit(i)) {
        R = R.add(both)
      } else {
        R = R.add(this)
      }
    } else {
      if (k.testBit(i)) {
        R = R.add(x)
      }
    }
    --i
  }

  return R
}

ECPointFp.prototype.getX = pointFpGetX
ECPointFp.prototype.getY = pointFpGetY
ECPointFp.prototype.equals = pointFpEquals
ECPointFp.prototype.isInfinity = pointFpIsInfinity
ECPointFp.prototype.negate = pointFpNegate
ECPointFp.prototype.add = pointFpAdd
ECPointFp.prototype.twice = pointFpTwice
ECPointFp.prototype.multiply = pointFpMultiply
ECPointFp.prototype.multiplyTwo = pointFpMultiplyTwo

// ----------------
// ECCurveFp

// constructor
export function ECCurveFp(q, a, b) {
  this.q = q
  this.a = this.fromBigInteger(a)
  this.b = this.fromBigInteger(b)
  this.infinity = new ECPointFp(this, null, null)
  this.reducer = new Barrett(this.q)
}

function curveFpGetQ() {
  return this.q
}

function curveFpGetA() {
  return this.a
}

function curveFpGetB() {
  return this.b
}

function curveFpEquals(other) {
  if (other == this) return true
  return (
    this.q.equals(other.q) && this.a.equals(other.a) && this.b.equals(other.b)
  )
}

function curveFpGetInfinity() {
  return this.infinity
}

function curveFpFromBigInteger(x) {
  return new ECFieldElementFp(this.q, x)
}

function curveReduce(x) {
  this.reducer.reduce(x)
}

// for now, work with hex strings because they're easier in JS
function curveFpDecodePointHex(s) {
  switch (
    parseInt(s.substr(0, 2), 16) // first byte
  ) {
    case 0:
      return this.infinity
    case 2:
    case 3:
      // point compression not supported yet
      return null
    case 4:
    case 6:
    case 7:
      var len = (s.length - 2) / 2
      var xHex = s.substr(2, len)
      var yHex = s.substr(len + 2, len)

      return new ECPointFp(
        this,
        this.fromBigInteger(new BigInteger(xHex, 16)),
        this.fromBigInteger(new BigInteger(yHex, 16))
      )

    default:
      // unsupported
      return null
  }
}

function curveFpEncodePointHex(p) {
  if (p.isInfinity()) return '00'
  var xHex = p.getX().toBigInteger().toString(16)
  var yHex = p.getY().toBigInteger().toString(16)
  var oLen = this.getQ().toString(16).length
  if (oLen % 2 != 0) oLen++
  while (xHex.length < oLen) {
    xHex = '0' + xHex
  }
  while (yHex.length < oLen) {
    yHex = '0' + yHex
  }
  return '04' + xHex + yHex
}

ECCurveFp.prototype.getQ = curveFpGetQ
ECCurveFp.prototype.getA = curveFpGetA
ECCurveFp.prototype.getB = curveFpGetB
ECCurveFp.prototype.equals = curveFpEquals
ECCurveFp.prototype.getInfinity = curveFpGetInfinity
ECCurveFp.prototype.fromBigInteger = curveFpFromBigInteger
ECCurveFp.prototype.reduce = curveReduce
ECCurveFp.prototype.decodePointHex = curveFpDecodePointHex
ECCurveFp.prototype.encodePointHex = curveFpEncodePointHex
